# vim: set fenc=utf8:
import sys
import logging
import logger_config

from PyQt4.QtCore import *
from PyQt4.QtGui import *

from PyQt4 import uic

from serial_comm import *
from data_model import *
from graph_view import QwtHandler

form_class_batt_prefs, base_class_batt_prefs = uic.loadUiType('ui/battery_preferences.ui')
class BatteryPreferences(base_class_batt_prefs, form_class_batt_prefs):
	def __init__(self, parent=None):
		super().__init__(parent)
		self.setupUi(self)

		not_empty_re = QRegExp('.+')
		self.manufacturer.setValidator(QRegExpValidator(not_empty_re, self.manufacturer))
		self.type.setValidator(QRegExpValidator(not_empty_re, self.type))
		self.id.setValidator(QRegExpValidator(QRegExp('\w+'), None))

	@pyqtSlot()
	def on_id_changed(self):
		logging.info("id changed")

	def accept(self):
		for w in (self.manufacturer, self.type, self.id):
			if not w.hasAcceptableInput():
				w.setFocus()
				return
		super().accept()


form_class_open, base_class_open = uic.loadUiType('ui/preferences.ui')
class Preferences(base_class_open, form_class_open):
	def __init__(self, is_36=False, parent=None):
		super().__init__(parent)
		self.setupUi(self)
		if is_36:
			print("36V")
			self.discharge_voltage_limit.setMinimum(20)
			self.discharge_voltage_limit.setMaximum(36)
			self.charge_voltage_limit.setMinimum(36)
			self.charge_voltage_limit.setMaximum(48)

form_class_open, base_class_open = uic.loadUiType('ui/open_dialog.ui')
class OpenDialog(base_class_open, form_class_open):
	def __init__(self, parent=None):
		super().__init__(parent)
		self.setupUi(self)
		data = get_battery_files()
		for y, row in enumerate(data):
			self.battery_table.insertRow(y)
			for x, data in enumerate(row):
				self.battery_table.setItem(y, x, QTableWidgetItem(str(data)))
		self.battery_table.resizeColumnsToContents()
		try: self.battery_table.selectRow(0)
		except: pass

	def on_battery_table_itemDoubleClicked(self, item):
		self.battery_table.selectRow(item.row())
		self.accept()


form_class, base_class = uic.loadUiType('ui/main.ui')
class BatteryMain(base_class, form_class):
	def tr(self, text):
		#return QCoreApplication.translate("biqazo", text)
		return text

	status_changed = pyqtSignal((int,), (int, int, int))
	skip_requested = pyqtSignal(int)
	measure_arrived = pyqtSignal()
	cycle_started = pyqtSignal(int, int)

	def __init__(self, *args):
		super().__init__(*args)
		self.setupUi(self)

		self.stage_messages = {
			CYCLE_INITWAIT: self.tr("Ciklusok előtti üresjárati mérés"),
			CYCLE_DISCHARGE:self.tr("Kisütés folyamatban"),
			CYCLE_DISCHARGE_WAIT:self.tr("Kisütés befejezve, várakozás"),
			CYCLE_TEMPWATCH:self.tr("Akkumulátor hőmérséklet figyelés"),
			CYCLE_CHARGE:self.tr("Töltés"),
			CYCLE_CHARGE_WAIT:self.tr("Töltés befejezve, várakozás"),
			CYCLE_END:self.tr("Töltés vége"),
		}

		for i in range(1, 7):
			button = getattr(self, "stage_{}_skip".format(i))
			button.setVisible(False)

		self.progressBar = QProgressBar(visible=False)
		# status update signal
		self.status_changed[int].connect(self.on_status_changed)
		self.status_changed[int, int, int].connect(self.on_status_changed)
		# measure update signal
		self.measure_arrived.connect(self.on_measure_arrived)
		self.cycle_started.connect(self.on_cycle_started)
		# load ini file
		self.settings = QSettings("biQazo.ini", QSettings.IniFormat)

		self.is_36 = self.settings.value('advanced/36v', False, bool)

		# initialize preferences
		self.serial_ports = scan_serial()
		self.preferences = Preferences(self.is_36)

		self.preferences.charge_baud.setModel(BAUD_RATES_MODEL)
		self.preferences.display_time_scale.setModel(TIME_SCALE_MODEL)

		# XXX build model in data_model
		for n, s in self.serial_ports:
			self.preferences.charge_device.addItem(s, n)

		self.load_prefs()



		self.qwt = QwtHandler(self, self.graph, self.graph2, self.is_36)
	
	measure_active = False

	def on_measure_started(self):
		self.measure_active = True
		self.start_cycles.setEnabled(False)

	def on_measure_stopped(self):
		self.measure_active = False
		self.start_cycles.setEnabled(True)
		self.start_cycles.setChecked(False)
		del self.cycles
		logging.debug("returned from thread")

	def on_cycle_started(self, i, total):
		row_count = rows_in_table('cycles', self.battery_db)
		self.cycle_slider.setMaximum(row_count)
		self.cycle_spinbox.setMaximum(row_count)
		self.cycle_spinbox.setValue(row_count)
		self.cycles_progress.setValue(i)
		self.cycles_progress.setMaximum(total)
		self.show_cycle(row_count)

	@pyqtSlot(int)
	@pyqtSlot(int, int, int)
	def on_status_changed(self, stage, completed=None, total=None):
		""" Slot to update statusbar (and progressbar, cancel button) """
		if stage:
			self.statusBar().showMessage(self.stage_messages[stage])
			self.stage_label.setText(self.stage_messages[stage])
		if completed is None:
			for i in range(1, 7):
				label = getattr(self, 'stage_{}_label'.format(i))
				label.setEnabled(i == stage)
				label.setFrameShape(QFrame.Box if i == stage else QFrame.NoFrame)
				getattr(self, 'stage_{}_skip'.format(i)).setVisible(i == stage)
		if isinstance(completed, int) and isinstance(total, int):
			if not self.progressBar.isVisible():
				self.statusBar().addPermanentWidget(self.progressBar)
				self.progressBar.show()
			self.progressBar.setMinimum(0)
			self.progressBar.setMaximum(total)
			self.progressBar.setValue(completed)
		elif self.progressBar.isVisible():
				self.statusBar().removeWidget(self.progressBar)

	@pyqtSlot(int)
	def on_cycle_spinbox_valueChanged(self, i):
		self.show_cycle(i)

	def load_battery(self, filename):
		self.unload_battery()
		# set up db
		self.battery_db = BatteryDb.addSqlDb(filename)
		self.prefs_model = IniTable(self.battery_db, 'preferences')

		# init cycles model
		# self.cycles_model = QSqlTableModel(db=self.battery_db, beforeInsert=self.on_cycle_started) # FIXME
		cycles_model = QSqlTableModel(db=self.battery_db)  # FIXME
		cycles_model.setTable('cycles')
		cycles_model.setSort(cycles_model.fieldIndex('id'), Qt.AscendingOrder)
		cycles_model.select()
		self.cycles_model = cycles_model

		self.measure_model = QSqlTableModel(db=self.battery_db)  # FIXME
		self.measure_model.setTable('measure_data')
		self.measure_model.setSort(self.measure_model.fieldIndex('id'), Qt.DescendingOrder)
		self.measure_model.select()

		# adjust spinbox / slider
		row_count = rows_in_table('cycles', self.battery_db)
		#if row_count == 0: row_count += 1
		self.cycle_spinbox.setMinimum(1)
		self.cycle_spinbox.setMaximum(row_count)
		self.cycle_slider.setMinimum(1)
		self.cycle_slider.setMaximum(row_count)
		self.cycle_spinbox.setValue(row_count)

		self.start_cycles.setEnabled(True)
		self.start_cycles.setToolTip(None)

		title = 'biQázó'
		self.setWindowTitle('{progname} - {id} ({manufacturer} {type} {capacity}Ah)'.format(
			progname=title, id=self.prefs_model['id'], manufacturer=self.prefs_model['manufacturer'],
			type=self.prefs_model['type'], capacity=self.prefs_model['capacity']))

		self.show_cycle()

	def unload_battery(self):
		try:
			del self.prefs_model, self.cycles_model, self.measure_model, self.battery_db
		except: pass
		BatteryDb.removeSqlDb()

	def load_prefs(self):
		error = False
		for key in CYCLE_SETTINGS_SPINBOX:
			val = self.settings.value(key.replace('_', '/', 1))
			if val is not None:
				getattr(self.preferences, key).setValue(float(val))
			else:
				error = "Val {} is none".format(val)
		for key in CYCLE_SETTINGS_COMBO:
			try:
				val = int(self.settings.value(key.replace('_', '/', 1)))
				combo = getattr(self.preferences, key)
				idx = combo.findData(val)
				if idx != -1:
					combo.setCurrentIndex(idx)
			except Exception as e: error = "Error during key {}: {}".format(key, e)
		if error:
			QMessageBox.warning(None, "Betöltési hiba", "A beállítások betöltése nem volt zökkenőmentes\n(valaki piszkálta a beállításokat tartalmazó fájlt vagy ez a program első indulása).\nNéhány tulajdonság alapértékre állt.\nMenjen a beállításokhoz és mentse azokat a beállítások javításához!")
			print(error)

	def save_prefs(self):
		for key in CYCLE_SETTINGS_SPINBOX:
			val = getattr(self.preferences, key).value()
			# str(val) fixes the 12.299999999 in settings
			self.settings.setValue(key.replace('_', '/', 1), val)
		for key in CYCLE_SETTINGS_COMBO:
			combo = getattr(self.preferences, key)
			self.settings.setValue(key.replace('_', '/', 1),
				combo.itemData(combo.currentIndex()))
		self.settings.sync()

	def prefs_as_dict(self):
		res = dict()
		for key in CYCLE_SETTINGS_SPINBOX:
			val = getattr(self.preferences, key).value()
			# str(val) fixes the 12.299999999 in settings
			res[key.replace('_', '/', 1)] = val
		for key in CYCLE_SETTINGS_COMBO:
			combo = getattr(self.preferences, key)
			res[key.replace('_', '/', 1)] = combo.itemData(combo.currentIndex())
		return res

	@pyqtSlot()
	def on_actionPreferences_triggered(self):
		if self.preferences.exec_():
			self.save_prefs()
		else:
			self.load_prefs()

	@pyqtSlot()
	def on_actionAbout_triggered(self):
		QMessageBox.about(self, self.tr("A biQázóról"),
			self.tr("<h3>biQázó</h3>" +
			"<p>Akkumulátor kisütésére és töltésére szolgáló felület</p>" +
			"<p>A program licensze GPL v3</p>" +
			"<p>Az ingyenesen elérhető <a href="">famfamfam ikonokért</a> köszönet Mark Jamesnek<br />"
			"www.famfamfam.com/lab/icons/silk/</p>" +
			"<hr />"
			"<p>&copy;2010 Kontra, Gergely</p>"))

	@pyqtSlot()
	def on_actionOpen_triggered(self):
		if self.measure_active: return
		open_dialog = OpenDialog(self)
		if open_dialog.exec_():
			idx = open_dialog.battery_table.currentRow()
			fn = open_dialog.battery_table.item(idx, 5).text()
			self.load_battery(fn)

	@pyqtSlot()
	def on_actionNew_triggered(self):
		if self.measure_active: QMessageBox(self.tr("Mérés közben nem"))
		batt_pref = BatteryPreferences(self)
		if batt_pref.exec_():
			fn = create_battery_db(
				batt_pref.manufacturer.text(),
				batt_pref.type.text(),
				batt_pref.id.text(),
				batt_pref.capacity.value())
			self.load_battery(fn)

	@pyqtSlot()
	def on_actionExportGraph_triggered(self):
		idx = self.cycle_spinbox.value()
		if idx == 0: return
		filename = QFileDialog.getSaveFileName(self, caption='save', directory='.', filter='Excel files (*.xls)')
		if not filename: return
		export_cycles(filename, self.cycles_model, self.battery_db)

	# state changing actions

	@pyqtSlot()
	def on_start_cycles_clicked(self):
		num_cycles = self.charging_cycles.value()
		try:
			self.on_measure_started()
			use_mock = self.settings.value('advanced/use_mock', False, bool)
			self.cycles = Cycles(num_cycles, self.prefs_as_dict(), self.status_changed, self.measure_arrived, self.cycle_started, self.cycles_model, self.measure_model, use_mock)
			self.skip_requested.connect(self.cycles.on_skipped)
		except:
			self.on_measure_stopped()
			raise
		self.cycles.finished.connect(self.on_measure_stopped)
		self.cycles.start()

	@pyqtSlot()
	def on_stage_1_skip_clicked(self):
		logging.info("stage 1 skip request")
		self.skip_requested.emit(1)
	@pyqtSlot()
	def on_stage_2_skip_clicked(self):
		self.skip_requested.emit(2)
	@pyqtSlot()
	def on_stage_3_skip_clicked(self):
		self.skip_requested.emit(3)
	@pyqtSlot()
	def on_stage_4_skip_clicked(self):
		self.skip_requested.emit(4)
	@pyqtSlot()
	def on_stage_5_skip_clicked(self):
		self.skip_requested.emit(5)
	@pyqtSlot()
	def on_stage_6_skip_clicked(self):
		self.skip_requested.emit(6)

	def show_cycle(self, cycle_no=None):
		d = self.prefs_as_dict()
		time_scale = d['display/time_scale']
		t_max = d['display/time_max'] * 60
		def append_points(query, time_shift=0):
			rec = query.record() # empty record to figure out field positions
			voltage_idx = rec.indexOf('voltage')
			voltage2_idx = rec.indexOf('voltage2')
			current_idx = rec.indexOf('current')
			temperature_idx = rec.indexOf('temperature')
			ts_idx = rec.indexOf('ts')

			while query.next():
				self.time_data.append((query.value(ts_idx) + time_shift) / time_scale)
				self.voltage_data.append(query.value(voltage_idx))
				self.voltage2_data.append(query.value(voltage2_idx))
				self.current_data.append(query.value(current_idx))
				self.temperature_data.append(query.value(temperature_idx))

		if cycle_no is None: cycle_no = self.cycle_spinbox.value()
		if not cycle_no: return
		self.cycle_no = cycle_no - 1

		if cycle_no > 1:
			prev_cycle_id = self.cycles_model.record(cycle_no - 2).field('id').value()
		else:
			prev_cycle_id = None
		cycle_record = self.cycles_model.record(cycle_no - 1)
		for prop in CYCLE_SETTINGS_DISPLAYED:
			getattr(self, prop).setText(str(cycle_record.field(prop).value()))

		self.time_data = []
		self.voltage_data = []
		self.voltage_data
		self.voltage2_data = []
		self.current_data = []
		self.temperature_data = []

		if prev_cycle_id is not None:
			prev_cycle_len = cycle_length(prev_cycle_id, self.cycles_model.database())
			if prev_cycle_len is not None:
				append_points(measure_query(prev_cycle_id, self.cycles_model.database(), max(0, prev_cycle_len - OVERLAP_INTERVAL)), -prev_cycle_len)


		cycle_id = cycle_record.field('id').value()

		query = measure_query(cycle_id, self.cycles_model.database())
		append_points(query)

		next_cycle_id = self.cycles_model.record(cycle_no).field('id').value()
		cycle_len = cycle_length(cycle_id, self.cycles_model.database())

		self.qwt.clear_markers()
		if next_cycle_id is not None and cycle_len is not None:
			append_points(measure_query(next_cycle_id, self.cycles_model.database(), max_ts=OVERLAP_INTERVAL), cycle_len)

			self.qwt.set_markers(cycle_len/time_scale)

		self.qwt.reset_scales(time_scale, t_max)

		self.qwt.refresh_graph(self.time_data, self.voltage_data, self.voltage2_data, self.current_data, self.temperature_data)

	def on_measure_arrived(self):
		if self.battery_db is None:
			logging.info("Measure arrives, but no db opened")
			return
		d = self.prefs_as_dict()
		TIME_SCALE = d['display/time_scale']
		data = self.measure_model.record(0)
		if data.isEmpty():
			logging.info("Got nothing :(")
		self.voltage_display.setText(format(float(data.value("voltage")), ".2f"))
		self.voltage2_display.setText(format(float(data.value("voltage2")), ".2f"))
		self.current_display.setText(str(data.value("current")))
		self.temperature_display.setText(str(data.value("temperature")))
		t = int(data.value("ts"))
		t, sec = divmod(t, 60)
		hour, minute = divmod(t, 60)
		self.cycle_time_display.setText("{:2d}:{:02d}:{:02d}".format(hour, minute, sec))
		displayed_cycle_id = self.cycles_model.record(self.cycle_spinbox.value() - 1).value('id')
		measure_cycle_id = data.value('cycle_id')
		# logging.info("displayed cycle id: %s, measure is from: %s", displayed_cycle_id, measure_cycle_id)
		if displayed_cycle_id == measure_cycle_id:
			self.time_data.append(data.value('ts') / TIME_SCALE)
			self.voltage_data.append(data.value('voltage'))
			self.voltage2_data.append(data.value('voltage2'))
			self.current_data.append(data.value('current'))
			self.temperature_data.append(data.value('temperature'))
			self.qwt.refresh_graph(self.time_data, self.voltage_data, self.voltage2_data, self.current_data, self.temperature_data)

	def closeEvent(self, event):
		logging.debug("Closing sqlite db")
		self.unload_battery()
		logging.debug("Bye %r", event)
		# event.ignore()

if __name__ == "__main__":
	app = QApplication(sys.argv)
	app.setStyle("plastique")
	window = BatteryMain()

	window.show()
	sys.exit(app.exec_())
